/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.internal;

import java.lang.reflect.*;
import java.util.*;
import ch.msoftch.internal.*;
import ch.simpleel.accessors.*;
import ch.simpleel.util.*;
import ch.trackedbean.copier.*;

/**
 * Object holding a list mapping.<br>
 * Only source to destination mapping is supported.
 * 
 * @author M. Hautle
 */
public class CollectionMappingHolder extends AbstractSimpleMappingHolder {
    /** The type evaluator to use for {@link #copyToSource(Object, Object, IMappingContext)}. */
    protected ITypeEvaluator dstEvaluator;

    /**
     * Default constructor.
     * 
     * @param src The source accessor
     * @param dst The destination accessor (on the object holding the mapping annotations)
     * @param evaluator The evaluator to use for the destination types
     * @param lazy Flag indicating if the property should be fetched lazy
     * @param profile The profile to which this mapping belongs
     */
    public CollectionMappingHolder(IValueAccessor src, IValueAccessor dst, ITypeEvaluator evaluator, boolean lazy, String profile) {
        super(src, dst, false, lazy, profile);
        if (evaluator == null)
            throw new IllegalArgumentException("The evaluator must be set!");
        dstEvaluator = evaluator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void copyToDestination(Object src, Object dst, IMappingContext context) {
        try {
            final Object srcValue = srcProp.getValue(src, null);
            // set the destination to null if the source was null
            if (srcValue == null) {
                dstProp.setValue(dst, null);
                return;
            }
            dstProp.setValue(dst, convertToDst(srcValue, dstProp.getValue(dst, null), context));
        } catch (Exception e) {
            throw new TechnicalException("Error while copying " + srcProp.getPath() + " to " + dstProp.getPath(), e);
        }
    }

    /**
     * Converts the given source into the corresponding destination.
     * 
     * @param srcValue The source value
     * @param existingDst The existing destination value or null
     * @param context The mapping context
     * @return The destination value
     */
    protected Object convertToDst(Object srcValue, Object existingDst, IMappingContext context) {
        final Collection src = convert(srcValue);
        final Class dstType = dstProp.getType();
        if (List.class == dstType)
            return convertToDst(src, new ArrayList(src.size()), context);
        if (Set.class == dstType)
            return convertToDst(src, new HashSet(), context);
        if (!dstType.isArray())
            throw new IllegalArgumentException("The destination type " + dstType.getName() + " is not supported!");
        // no existing array?
        if (existingDst == null)
            return convertToDst(src, (Object[]) Array.newInstance(dstType.getComponentType(), src.size()), 0, context);
        // Already existing array, we need to 'resize' it
        final Object[] old = (Object[]) existingDst;
        return convertToDst(src, MiscUtils.arrayExpand(old, src.size()), old.length, context);
    }

    /**
     * 'Converts' the given source to a collection.
     * 
     * @param src The source
     * @return The source as collection
     */
    protected Collection convert(Object src) {
        if (src instanceof Collection)
            return (Collection) src;
        if (src instanceof Object[])
            return Arrays.asList((Object[]) src);
        throw new IllegalArgumentException("The source type " + src.getClass().getName() + " is not supported!");
    }

    /**
     * Converts the given source into the corresponding destination collection.
     * 
     * @param src The source collection
     * @param dst The destination collection
     * @param context The mapping context
     * @return The destination collection
     */
    @SuppressWarnings("unchecked")
    protected Collection convertToDst(Collection src, Collection dst, IMappingContext context) {
        for (Object o : src)
            dst.add(convertToDstEntry(o, context));
        return dst;
    }

    /**
     * Converts the given source into the corresponding destination array.
     * 
     * @param src The source collection
     * @param dst The destination array
     * @param i The index to use for the first entry
     * @param context The mapping context
     * @return The destination array
     */
    private Object convertToDst(Collection src, Object[] dst, int i, IMappingContext context) {
        for (Object o : src)
            dst[i++] = convertToDstEntry(o, context);
        return dst;
    }

    /**
     * Converts a source object to a destination object.
     * 
     * @param o The src object to convert
     * @param context The mapping context
     * @return The converted source object
     */
    protected Object convertToDstEntry(Object o, IMappingContext context) {
        final Class<?> dstType = dstEvaluator.getCorrespondingType(o);
        // lookup already converted objects
        final Object val = context.getElement(o, dstType);
        if (val != null)
            return val;
        // create and convert then (the mapper will register the instance in the context)
        final IBeanMapper m = BeanMapperManager.getMapper(dstType);
        return m.copySrc2Dst(o, create(dstType), context);
    }

    /**
     * Creates an instance of the given type.
     * 
     * @param type The type to intanciate
     * @return An instance fo the given type
     */
    private Object create(final Class<?> type) {
        try {
            return type.newInstance();
        } catch (Exception e) {
            throw new TechnicalException("Error while trying to instanciate " + type.getName(), e);
        }
    }

    /**
     * Does nothing.
     * 
     * @param dst not used
     * @param src not used
     * @param context not used
     */
    @Override
    public void copyToSource(Object src, Object dst, IMappingContext context) {
    }

    /**
     * Does nothing.
     * 
     * @return null
     */
    @Override
    public Object getDstValue(Object dst) {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public Object getSrcValue(Object src) {
        final Object srcValue = srcProp.getValue(src, null);
        if (srcValue == null)
            return null;
        return convertToDst(srcValue, null, NOPMappingContext.NOP);
    }
}